<template>
  <el-upload
    v-model:file-list="fileList"
    :before-upload="beforeUpload"
    :http-request="handleUpload"
  >
    <el-button type="primary">点击上传</el-button>
    <template #tip>
      <div class="el-upload__tip">上传文件大小不能超过1GB</div>
    </template>
  </el-upload>
</template>

<script lang="ts">
export default {
  name: "Upload",
};
</script>

<script lang="ts" setup>
import { ref } from "vue";
import { ElMessage } from "element-plus";
import type {
  UploadProps,
  UploadRequestOptions,
} from "element-plus";
import type {
  ChunkFileList,
  UploadCustomFile,
  OnProgress,
  RequestHandler,
} from "./type";

// 已上传的文件列表
const fileList = ref<UploadCustomFile[]>([]);

// 在准备上传前检查文件大小
const beforeUpload: UploadProps["beforeUpload"] = (file) => {
  if (file.size / 1024 / 1024 / 1024 > 1) {
    ElMessage.error("上传大小不能超过1G!");
    return false;
  }
  return true;
};

// 进行文件的多切片上传
  // file: 要上传文件的文件数据对象
const handleUpload = async ({file}: UploadRequestOptions) => {

  // 1. 生成生成文件的hash (启动分线程处理)
  const hash = await createHash(file);

  // 2. 生成文件切片
  const chunkFileList = createChunkFileList(file);
  const name = file.name;
  // 将每个切片的数据进行维护成一个包括该切片文件，切片名的对象
  const transformFileList = transformChunkFileList(chunkFileList, name, hash);

  // 上传之前，初始化上传进度
  fileList.value.forEach((file) => {
    if (file.name === name) {
      file.loaded = 0;
      file.status = "uploading";
    }
  });

  // 3. 确认上传切片情况
  const { data } = await verifyRequest(name, hash);
  // 如果标识不需要上传, 说明此文件已完整上传过, 提示已上传过 => 秒传
  if (!data.needUpload) {
    alert('文件已上传过: ' + data.url)
    return;
  }

  // 4. 上传切片
  await uploadChunks(transformFileList, name, data.chunkFileList);
  
  // 5. 合并
  const res = await mergeRequest(name, hash);
  console.log(res);
};

// 生成文件hash的函数: 使用webWorker在分线程利用md5库生成
const createHash = (file: File): Promise<string> => {
  return new Promise((resolve) => {
    // 创建worker
    const worker = new Worker(
      new URL("./createHashWorker.js", import.meta.url),
      {
        type: "module",
      }
    );
    // 向分线程发送消息, 携带要上传的文件对象数据
    worker.postMessage(file);
    // 绑定接收消息的监听
    worker.onmessage = (e) => {
      // 得到文件的hash值, 将promise变为成功的, value为hash值
      const { hash } = e.data;
      if (hash) {
        resolve(hash);
      }
    };
  });
};

// 自定义切片大小 10mb
const CHUNK_SIZE = 10 * 1024 * 1024;

// 生成文件切片列表
const createChunkFileList = (file: File, size = CHUNK_SIZE) => {
  const chunkFileList = [];
  let currentSize = 0;
  while (currentSize < file.size) {
    // 第一次 0 - 10mb
    // 第二次 10mb - 20mb
    // ...
    chunkFileList.push(file.slice(currentSize, currentSize + size));
    currentSize += size;
  }
  return chunkFileList;
};

// 将切片的数据进行维护成一个包括该切片文件，切片名的对象
const transformChunkFileList = (
  chunkFileList: Blob[],
  name: string,
  hash: string
) => {
  return chunkFileList.map((chunkFile, index) => {
    return {
      chunk: chunkFile, // 切片文件
      hash: `${name}-${hash}-${index}`, // 切片名
    };
  });
};

// 自定义上传切片的方法
const request: RequestHandler = ({
  url,
  data,
  method = "post",
  headers = {},
  onProgress,
}) => {
  return new Promise((resolve, reject) => {
    // 创建xhr对象
    const xhr = new XMLHttpRequest();
    // 初始化请求, 指定method和url
    xhr.open(method, url);
    // 绑定上传进度的回调
    if (onProgress) {
      xhr.upload.onprogress = onProgress;
    }
    // 添加所有请求头参数
    Object.keys(headers).forEach((key) =>
      xhr.setRequestHeader(key, headers[key])
    );
    // 绑定请求完成的监听
    xhr.addEventListener("load", (e) => {
      // 如果响应状态码不在200到299间, 将promise变为失败的
      if (xhr.status < 200 || xhr.status >= 300) {
        reject("失败");
      }
      // 将promise变为成功的, 并将响应体数据指定为成功的value
      resolve(JSON.parse(xhr.responseText));
    });
    // 发送请求
    xhr.send(data);
  });
};

// 处理上传请求进度变化, 更新显示进度
const handleProgress = (name: string): OnProgress => {
  return (e) => {
    console.log('监听到进度变化', e.loaded / e.total)
    // 遍历找到对应的file
    fileList.value.forEach((file) => {
      // 只有当切片已经上传完成才更新进度条
      if (file.name === name && e.loaded===e.total) {
        // 指定已经上传文件的大小
        file.loaded += e.loaded;
        // 更新文件的进度百分数
        file.percentage = Number(((file.loaded / (file.size as number)) * 100).toFixed(0));
        // 如果已全部上传, 更新文件状态为success
        if (file.percentage === 100) {
          file.status = "success";
        }
      }
    });
  };
};

// 确认上传情况的请求
const verifyRequest = (filename: string, hash: string) => {
  return request({
    url: "http://localhost:3000/verify/file",
    headers: { "content-type": "application/json" },
    data: JSON.stringify({
      filename,
      fileHash: hash,
    }),
  });
};

// 上传多个切片的请求
const uploadChunks = async (
  fileList: ChunkFileList,
  name: string,
  chunkFileList: string[] = [] // 已经上传好的切片
) => {
  const requestList = fileList
    // 过滤出需要上传的file列表
    .filter((file) => {
      return !chunkFileList.some(chunkFile => file.hash.includes(chunkFile));
    })
    .map(({ chunk, hash }) => {
      // 用FormData封装要上传切片请求的参数
      const formData = new FormData();
      formData.append("chunk", chunk, hash); // 参数名 切片文件数据 切片文件名
      formData.append("filename", name); // 参数名 总文件名
      return formData;
    })
    .map((formData) => {
      // 发送切片文件上传请求, 返回promise
      return request({
        url: "http://localhost:3000/upload/file",
        data: formData, // 包含切片文件的表单数据
        onProgress: handleProgress(name), // 绑定切片请求进度改变的回调
      })
    });

  // 并发请求
  await Promise.all(requestList);
};

// 通知服务器合并上传切片成一个文件的请求
const mergeRequest = (name: string, hash: string) => {
  return request({
    method: 'post',
    url: "http://localhost:3000/merge/file",
    headers: { "content-type": "application/json" },
    data: JSON.stringify({
      filename: name,
      fileHash: hash,
    }),
  });
};
</script>

<style scoped></style>
